The hard ways

Generator Function

目前在我的玩具 JS 引擎 naive 中还没有实现 Generator,所以这篇除了可以当做是预先调研之外,也可以作为稍微深入到 Generator 实现细节的介绍

普通函数

由于 Generator Function 和普通函数长得太像了,所以先回顾一下普通函数实现细节

我们知道在 C 语言中函数最终体现为代码段中的一段内容,该段内容就是函数体内容对应的机器码,而函数名则为该段代码的首地址,在编译期间会被替换处理

但是在学习 JS 的时候,并不是每个地方都能以 C 语言来参考,这里把 C 换成任何其他语言也说得通;可以在一些问题发生的时候,使用已经掌握的知识来参考联想,但是最终还是要回归到该语言自身的技术规格文档

在 JS 中,函数作为一个对象,该对象包含了以下几个内容:

  1. 形参信息
  2. 函数体内容所对应的字节码
  3. 与之关联的闭包

当我们调用函数的时候,引擎就会做以下的事情:

  1. 创建一个表示调用信息的对象 CallInfo
  2. 将第一步创建的对象添加到调用栈中
  3. 引擎根据调用栈顶层的调用信息继续执行

CallInfo,包含下面的内容:

  1. 调用的函数对象
  2. PC,表示接下来需要被执行的字节码的地址
  3. this 对象,用于在执行 THIS 指令的时候取得对应的对象

另外 JS 引擎在运行时会用到两个栈结构,一个作为调用栈,一个作为操作数栈我们上面介绍的是调用栈,而操作数栈用于存放指令执行时所用到的操作数,包括局部变量和临时变量

生成器函数

我们来看一个典型的生成器函数的例子:

function* idMaker() {
  var index = 0;
  while(true)
    yield index++;
}

想必大家第一次接触到这个语法的时候一头雾水,因为一直以来 while(true){ /* no break or return */ } 这样的形式,直接告诉我们该循环为一个死循环恰好上面的例子中,while 语句中也没有 breakreturn,如果是死循环,那么这段代码肯定就失去意义了,如果不是死循环,又打破了我们之前的认知

其实 yield 语句,不过是一个语法糖(Syntactic sugar)所谓语法糖就是一些方便程序员书写代码的语法,它们总能找到不使用该语法的对应写法如果我们参考了技术规格文档,那么发现其实 yield 隐含了 return 的语义,知道这个就放心了,原来我们之前的认知是准确无误的

在了解了 yield 隐含了 return 的语义后,死循环的问题可以先不用考虑了,但是产生了一个新的问题:在普通函数中,一旦 return 出去了,等于函数主动放弃了执行权,那么再没有办法恢复之前的执行状态但是我们的生成器函数,是可以不断执行的,换句话说,在 yield 隐含的 return 语义生效后依然可以保持执行状态,以便下一次执行时从上一次暂停点继续执行

因为 JS 引擎是一个单线程的引擎,所以同一时间内,只有一段 JS 代码(函数)会被执行,换句话说,这段时间内执行的代码就占据了程序(引擎)的控制权,而当当前函数(callee)执行完毕后,引擎转而继续执行 caller 中的内容,就称函数交出了对引擎的控制权我们可以显式地交出控制权,通过显式地 return 语句;或者隐式地交出控制权,通过编译器在每个函数末尾自动插入的 return 语句之所以普通函数 return 之后无法再之前恢复状态是因为,保存之前调用信息的 CallInfo 对象已经从调用栈中被移除了,也就无法知道之前的 PC 等信息了

不知道大家看到这里会不会有灵光一闪的感觉:如果 CallInfo 对象在调用结束后,能以一种方式保存它,那么后续再次调用它时,不就能继续执行了吗?没错,生成器函数本质上就是这个道理

Made with gadget